package com.bondar.task3;

import java.sql.Array;
import java.sql.Blob;
import java.sql.CallableStatement;
import java.sql.Clob;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.DriverManager;
import java.sql.NClob;
import java.sql.PreparedStatement;
import java.sql.SQLClientInfoException;
import java.sql.SQLException;
import java.sql.SQLWarning;
import java.sql.SQLXML;
import java.sql.Savepoint;
import java.sql.Statement;
import java.sql.Struct;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
import java.util.Queue;
import java.util.concurrent.Executor;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;

public class ConnectionPoolImpl implements ConnectionPool {

	private static final String COM_MYSQL_JDBC_DRIVER = "org.h2.Driver";
	private static final Logger logConnectionPool = Logger.getLogger("ConnectionPoolImpl");

	private final String url;
	private final String user;
	private final String password;
	private final Semaphore semaphore;
	private final Connection[] connections;
	private final ConnectionCollector collector;
	
	
	private Queue<MyConnection> inUse;
	private Queue<MyConnection> unUsed;

	/**
	 * Create Connection Pool with n Connections
	 * 
	 * @param url
	 * @param user
	 * @param password
	 */
	public ConnectionPoolImpl(String url, String user, String password) {

		try {
			Class.forName(COM_MYSQL_JDBC_DRIVER).newInstance();
		} catch (Exception e) {
			e.printStackTrace();
		}

		this.semaphore = new Semaphore(N, true);
		this.url = url;
		this.user = user;
		this.password = password;
		this.connections = new Connection[N];
		this.collector = new ConnectionCollector();
		
		inUse = new LinkedBlockingQueue<MyConnection>();
		unUsed = new LinkedBlockingQueue<MyConnection>();

		for (int i = 0; i < N; i++) {
			Connection conn = createConnection();
			connections[i] = conn;
			unUsed.offer(new MyConnection(conn, System.currentTimeMillis()));
		}
		collector.start();
	}

	protected Connection createConnection() {
		Connection conn = null;
		try {
			conn = DriverManager.getConnection(url,user,password);
		} catch (SQLException e) {
			e.printStackTrace();
		}

		return conn;
	}

	@Override
	public Connection checkOut() throws Exception {
		try {
			if (!semaphore.tryAcquire(WAITING_TIMEOUT, TimeUnit.MILLISECONDS)) {
				collector.interrupt();

				logConnectionPool.log(Level.WARNING, "Exprired WAITING_TIMEOUT = "
						+ WAITING_TIMEOUT);
				throw new Exception();
			}
		} catch (InterruptedException e) {
			logConnectionPool.log(Level.SEVERE, "InterruptedException: " + e);
		}

		synchronized (this) {
			// try to get connection
			MyConnection conn = unUsed.remove();
			conn.setCreatedTime(System.currentTimeMillis());
			inUse.offer(conn);
			
			return conn;
		}

	}

	@Override
	public void checkIn(Connection connection) {
		try {
			logConnectionPool.log(Level.INFO, "Try to checkIn");
			if (inUse.remove(connection)) {
				connection.close();
				unUsed.offer((MyConnection) connection);
				logConnectionPool.log(Level.INFO, "Connection returned to pool");
				semaphore.release();
			}
		} catch (SQLException e) {
			e.printStackTrace();
		}
	}


	@Override
	public void close() {
		try {
			logConnectionPool.log(Level.INFO, "Closing Connection Pool");
			collector.interrupt();
			closeAllConnections();
		} catch (SQLException e) {
			e.printStackTrace();
		}
	}

	private void closeAllConnections() throws SQLException {
		
		Iterator<MyConnection> it = inUse.iterator();
		while (it.hasNext()) {
			checkIn(it.next());
		}
		
		for (int i = 0; i < connections.length; i++) {
			Connection conn = connections[i];
			if (conn != null && !conn.isClosed()) {
				conn.close();
			}
		}
	}

	private static class MyConnection implements Connection {

		private Connection connection;
		private Long createdTime;

		public MyConnection(Connection connection, long start) {
			this.connection = connection;
			this.createdTime = start;
		}

		public void setConnection(Connection connection) {
			this.connection = connection;
		}

		public Connection getConnection() {
			return connection;
		}

		public Long getCreatedTime() {
			return createdTime;
		}

		public void setCreatedTime(Long createdTime) {
			this.createdTime = createdTime;
		}

		public void updateCreatedTime() {
			this.createdTime = System.currentTimeMillis();
		}

		@Override
		public <T> T unwrap(Class<T> iface) throws SQLException {
			updateCreatedTime();
			return connection.unwrap(iface);
		}

		@Override
		public boolean isWrapperFor(Class<?> iface) throws SQLException {
			updateCreatedTime();
			return connection.isWrapperFor(iface);
		}

		@Override
		public Statement createStatement() throws SQLException {
			updateCreatedTime();
			return connection.createStatement();
		}

		@Override
		public PreparedStatement prepareStatement(String sql)
				throws SQLException {
			updateCreatedTime();
			return connection.prepareStatement(sql);
		}

		@Override
		public CallableStatement prepareCall(String sql) throws SQLException {
			updateCreatedTime();
			return connection.prepareCall(sql);
		}

		@Override
		public String nativeSQL(String sql) throws SQLException {
			updateCreatedTime();
			return connection.nativeSQL(sql);
		}

		@Override
		public void setAutoCommit(boolean autoCommit) throws SQLException {
			updateCreatedTime();
			connection.setAutoCommit(autoCommit);
		}

		@Override
		public boolean getAutoCommit() throws SQLException {
			updateCreatedTime();
			return connection.getAutoCommit();
		}

		@Override
		public void commit() throws SQLException {
			updateCreatedTime();
			connection.commit();
		}

		@Override
		public void rollback() throws SQLException {
			updateCreatedTime();
			connection.rollback();
		}

		@Override
		public void close() throws SQLException {
			connection = null;
			logConnectionPool.log(Level.INFO, "override close() invoked");
		}

		@Override
		public boolean isClosed() throws SQLException {
			updateCreatedTime();
			return connection == null ? true : false;
		}

		@Override
		public DatabaseMetaData getMetaData() throws SQLException {
			updateCreatedTime();
			return connection.getMetaData();
		}

		@Override
		public void setReadOnly(boolean readOnly) throws SQLException {
			updateCreatedTime();
			connection.setReadOnly(readOnly);
		}

		@Override
		public boolean isReadOnly() throws SQLException {
			updateCreatedTime();
			return connection.isReadOnly();
		}

		@Override
		public void setCatalog(String catalog) throws SQLException {
			updateCreatedTime();
			connection.setCatalog(catalog);
		}

		@Override
		public String getCatalog() throws SQLException {
			updateCreatedTime();
			return connection.getCatalog();
		}

		@Override
		public void setTransactionIsolation(int level) throws SQLException {
			updateCreatedTime();
			connection.setTransactionIsolation(level);
		}

		@Override
		public int getTransactionIsolation() throws SQLException {
			updateCreatedTime();
			return connection.getTransactionIsolation();
		}

		@Override
		public SQLWarning getWarnings() throws SQLException {
			updateCreatedTime();
			return connection.getWarnings();
		}

		@Override
		public void clearWarnings() throws SQLException {
			updateCreatedTime();
			connection.clearWarnings();
		}

		@Override
		public Statement createStatement(int resultSetType,
				int resultSetConcurrency) throws SQLException {
			updateCreatedTime();
			return connection.createStatement(resultSetType,
					resultSetConcurrency);
		}

		@Override
		public PreparedStatement prepareStatement(String sql,
				int resultSetType, int resultSetConcurrency)
				throws SQLException {
			updateCreatedTime();
			return connection.prepareStatement(sql, resultSetType,
					resultSetConcurrency);
		}

		@Override
		public CallableStatement prepareCall(String sql, int resultSetType,
				int resultSetConcurrency) throws SQLException {
			updateCreatedTime();
			return connection.prepareCall(sql, resultSetType,
					resultSetConcurrency);
		}

		@Override
		public Map<String, Class<?>> getTypeMap() throws SQLException {
			updateCreatedTime();
			return connection.getTypeMap();
		}

		@Override
		public void setTypeMap(Map<String, Class<?>> map) throws SQLException {
			updateCreatedTime();
			connection.setTypeMap(map);
		}

		@Override
		public void setHoldability(int holdability) throws SQLException {
			updateCreatedTime();
			connection.setHoldability(holdability);
		}

		@Override
		public int getHoldability() throws SQLException {
			updateCreatedTime();
			return connection.getHoldability();
		}

		@Override
		public Savepoint setSavepoint() throws SQLException {
			updateCreatedTime();
			return connection.setSavepoint();
		}

		@Override
		public Savepoint setSavepoint(String name) throws SQLException {
			updateCreatedTime();
			return connection.setSavepoint(name);
		}

		@Override
		public void rollback(Savepoint savepoint) throws SQLException {
			updateCreatedTime();
			connection.rollback(savepoint);
		}

		@Override
		public void releaseSavepoint(Savepoint savepoint) throws SQLException {
			updateCreatedTime();
			connection.releaseSavepoint(savepoint);
		}

		@Override
		public Statement createStatement(int resultSetType,
				int resultSetConcurrency, int resultSetHoldability)
				throws SQLException {
			updateCreatedTime();
			return connection.createStatement(resultSetType,
					resultSetConcurrency, resultSetHoldability);
		}

		@Override
		public PreparedStatement prepareStatement(String sql,
				int resultSetType, int resultSetConcurrency,
				int resultSetHoldability) throws SQLException {
			updateCreatedTime();
			return connection.prepareStatement(sql, resultSetType,
					resultSetConcurrency, resultSetHoldability);
		}

		@Override
		public CallableStatement prepareCall(String sql, int resultSetType,
				int resultSetConcurrency, int resultSetHoldability)
				throws SQLException {
			updateCreatedTime();
			return connection.prepareCall(sql, resultSetType,
					resultSetConcurrency, resultSetHoldability);
		}

		@Override
		public PreparedStatement prepareStatement(String sql,
				int autoGeneratedKeys) throws SQLException {
			updateCreatedTime();
			return connection.prepareStatement(sql, autoGeneratedKeys);
		}

		@Override
		public PreparedStatement prepareStatement(String sql,
				int[] columnIndexes) throws SQLException {
			updateCreatedTime();
			return connection.prepareStatement(sql, columnIndexes);
		}

		@Override
		public PreparedStatement prepareStatement(String sql,
				String[] columnNames) throws SQLException {
			updateCreatedTime();
			return connection.prepareStatement(sql, columnNames);
		}

		@Override
		public Clob createClob() throws SQLException {
			updateCreatedTime();
			return connection.createClob();
		}

		@Override
		public Blob createBlob() throws SQLException {
			updateCreatedTime();
			return connection.createBlob();
		}

		@Override
		public NClob createNClob() throws SQLException {
			updateCreatedTime();
			return connection.createNClob();
		}

		@Override
		public SQLXML createSQLXML() throws SQLException {
			updateCreatedTime();
			return connection.createSQLXML();
		}

		@Override
		public boolean isValid(int timeout) throws SQLException {
			updateCreatedTime();
			return connection.isValid(timeout);
		}

		@Override
		public void setClientInfo(String name, String value)
				throws SQLClientInfoException {
			updateCreatedTime();
			connection.setClientInfo(name, value);
		}

		@Override
		public void setClientInfo(Properties properties)
				throws SQLClientInfoException {
			updateCreatedTime();
			connection.setClientInfo(properties);
		}

		@Override
		public String getClientInfo(String name) throws SQLException {
			updateCreatedTime();
			return connection.getClientInfo(name);
		}

		@Override
		public Properties getClientInfo() throws SQLException {
			updateCreatedTime();
			return connection.getClientInfo();
		}

		@Override
		public Array createArrayOf(String typeName, Object[] elements)
				throws SQLException {
			updateCreatedTime();
			return connection.createArrayOf(typeName, elements);
		}

		@Override
		public Struct createStruct(String typeName, Object[] attributes)
				throws SQLException {
			updateCreatedTime();
			return connection.createStruct(typeName, attributes);
		}

		@Override
		public void setSchema(String schema) throws SQLException {
			updateCreatedTime();
			connection.setSchema(schema);
		}

		@Override
		public String getSchema() throws SQLException {
			updateCreatedTime();
			return connection.getSchema();
		}

		@Override
		public void abort(Executor executor) throws SQLException {
			updateCreatedTime();
			connection.abort(executor);
		}

		@Override
		public void setNetworkTimeout(Executor executor, int milliseconds)
				throws SQLException {
			updateCreatedTime();
			connection.setNetworkTimeout(executor, milliseconds);
		}

		@Override
		public int getNetworkTimeout() throws SQLException {
			updateCreatedTime();
			return connection.getNetworkTimeout();
		}
	}

	private class ConnectionCollector extends Thread {
		
		public ConnectionCollector() {
			setDaemon(true);
		}

		@Override
		public void run() {
			MyConnection connection;
			logConnectionPool.log(Level.INFO, Thread.currentThread() + ": Collector started");

			while (!isInterrupted()) {
				
				Iterator<MyConnection> iterator = inUse.iterator();
				while (iterator.hasNext()) {
					connection = iterator.next();
					checkInExpired(connection);
				}

				if (isInterrupted()) {	}
			}
		}

		private void checkInExpired(MyConnection myConnection) {
			long expired = System.currentTimeMillis()
					- myConnection.getCreatedTime();
			if (expired >= IDLE_TIMEOUT) {
				logConnectionPool.log(Level.INFO, "Try to return connection");
				checkIn(myConnection);
			}

		}
	}

}
